/**
 * 
 */
package org.ycsoft.MetroidReader;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Represents a reader class for the metroid file format. This class exposes all
 * needed functionnality to consumes the information contained in metroid files.
 * 
 * @author allad
 * 
 */
public class MetroidReader implements Serializable {
	/**
	 * 
	 */
	private static final long serialVersionUID = 7287988031930734322L;
	private String filepath;
	transient private MetroidStream filestream;

	private boolean streamClosed = true;

	private MetroidMetadata metadata;

	private Map<Integer, Long> indexStopToPosition = new HashMap<Integer, Long>();
	private Map<Integer, Long> indexStopToTree = new HashMap<Integer, Long>();
	private Map<Integer, Long> indexLineToPosition = new HashMap<Integer, Long>();
	private Map<String, Integer> indexNameToId = new HashMap<String, Integer>();

	private long filepointer = 0; // used for serialization

	/**
	 * Opens a Metroid file.
	 * 
	 * @param filepath
	 *            Path name of file to be opened
	 */
	public MetroidReader(String filepath) throws IOException {
		this.filepath = filepath;

		filestream = new MetroidStream(filepath);
		streamClosed = false;
		metadata = new MetroidMetadata();

	}

	private void writeObject(ObjectOutputStream out) throws IOException {
		filepointer = filestream.getFilePointer();
		out.defaultWriteObject();
	}

	private void readObject(ObjectInputStream in) throws IOException,
			ClassNotFoundException {
		// our "pseudo-constructor"
		in.defaultReadObject();
		filestream = new MetroidStream(filepath);
		filestream.seek(filepointer);

	}

	/**
	 * All indexes are loaded if the openAll parameter is set to true otherwise
	 * only the metadata is loaded
	 * 
	 * @param loadAll
	 *            Specifies whether to load all indexes
	 * @throws IOException
	 */
	public void loadIndexes(boolean loadAll) throws IOException {
		// load metadata
		loadMetadata();

		if (loadAll) {
			// load indexes
			loadLinesIndex();
			loadStopsIndex();
			loadStopsNamesIndex();
		}
	}

	/**
	 * @return True if file stream is closed and IO operations is no more
	 *         allowed. False otherwise
	 */
	public boolean isStreamClosed() {
		return streamClosed;
	}

	/**
	 * Opens the file stream if it's not already opened
	 * 
	 * @throws IOException
	 */
	public void openStream() throws IOException {
		if (streamClosed) {
			filestream = new MetroidStream(filepath);
			streamClosed = false;
		}
	}

	/**
	 * Closes the file stream
	 * 
	 * @throws IOException
	 */
	public void closeStream() throws IOException {
		filestream.close();
		streamClosed = true;
	}

	/**
	 * @return An object containing metroids metadata
	 */
	public MetroidMetadata getMetadata() {
		return metadata;
	}

	/**
	 * Load the metadata from the metroid file
	 */
	private void loadMetadata() throws IOException {
		// go to end of file
		filestream.seek(0, SeekOrigin.END);

		String s;

		do {
			s = filestream.readPreviousLineAsString();
			s = s.trim();
		} while (!s.equals("metadata"));

		// now verify if the existing dictionary is empty
		// if so try to read the trailer from the end
		boolean cont = true;
		do {
			try {
				s = filestream.readLine();
				s = s.trim();

				if (s.equals("end")) // found end
				{
					cont = false; // end of metada section. break out of loop
				} else if (s.startsWith("city")) {
					if (s.contains(Commons.SEPARATOR + "")) {
						String[] tmp = s.split(Commons.SEPARATOR + "");
						if (tmp.length >= 2) {
							metadata.setCity(tmp[1].trim());
						}
					}
				} else if (s.startsWith("lang")) {
					if (s.contains(Commons.SEPARATOR + "")) {
						String[] tmp = s.split(Commons.SEPARATOR + "");
						if (tmp.length >= 2) {
							metadata.setLanguage(tmp[1].trim());
						}
					}
				} else if (s.startsWith("mean")) {
					if (s.contains(Commons.SEPARATOR + "")) {
						String[] tmp = s.split(Commons.SEPARATOR + "");
						if (tmp.length >= 2) {
							metadata.setMean(tmp[1].trim());
						}
					}
				} else if (s.startsWith("version")) {
					if (s.contains(Commons.SEPARATOR + "")) {
						String[] tmp = s.split(Commons.SEPARATOR + "");
						if (tmp.length >= 2) {
							metadata.setVersion(tmp[1].trim());
						}
					}
				} else if (s.startsWith("author")) {
					if (s.contains(Commons.SEPARATOR + "")) {
						String[] tmp = s.split(Commons.SEPARATOR + "");
						if (tmp.length >= 2) {
							metadata.setAuthor(tmp[1].trim());
						}
					}
				} else if (s.startsWith("indexstopsname")) {
					if (s.contains(Commons.SEPARATOR + "")) {
						String[] tmp = s.split(Commons.SEPARATOR + "");
						if (tmp.length >= 2) {
							int pos;
							try {
								pos = Integer.parseInt(tmp[1]);
								metadata.setStopsNamesIndexPosition(pos);
							} catch (NumberFormatException ne) {
								// ignore
							}
						}
					}
				} else if (s.startsWith("numberstops")) {
					if (s.contains(Commons.SEPARATOR + "")) {
						String[] tmp = s.split(Commons.SEPARATOR + "");
						if (tmp.length >= 2) {
							int pos;
							try {
								pos = Integer.parseInt(tmp[1]);
								metadata.setNbStops(pos);
							} catch (NumberFormatException ne) {
								// ignore
							}
						}
					}
				} else if (s.startsWith("numberlines")) {
					if (s.contains(Commons.SEPARATOR + "")) {
						String[] tmp = s.split(Commons.SEPARATOR + "");
						if (tmp.length >= 2) {
							int pos;
							try {
								pos = Integer.parseInt(tmp[1]);
								metadata.setNbLines(pos);
							} catch (NumberFormatException ne) {
								// ignore
							}
						}
					}
				} else if (s.startsWith("indexstops")) {
					if (s.contains(Commons.SEPARATOR + "")) {
						String[] tmp = s.split(Commons.SEPARATOR + "");
						if (tmp.length >= 2) {
							int pos;
							try {
								pos = Integer.parseInt(tmp[1]);
								metadata.setStopsIndexPosition(pos);
							} catch (NumberFormatException ne) {
								// ignore
							}
						}
					}
				} else if (s.startsWith("indexlines")) {
					if (s.contains(Commons.SEPARATOR + "")) {
						String[] tmp = s.split(Commons.SEPARATOR + "");
						if (tmp.length >= 2) {
							int pos;
							try {
								pos = Integer.parseInt(tmp[1]);
								metadata.setLinesIndexPosition(pos);
							} catch (NumberFormatException ne) {
								// ignore
							}
						}
					}
				}

			} catch (Exception e) {
				// if a trailer could not be read at the end
				// let's hope that it has been read peviously
				// so we go check if there's a /Root entry
				break;
			}
		} while (cont && s != null);
	}

	/**
	 * Loads the (LineId -> Position ) index
	 * 
	 * @throws IOException
	 */
	private void loadLinesIndex() throws IOException {
		if (metadata.getLinesIndexPosition() == -1) {
			throw new InvalidFileException("No lines index present");
		} else
			filestream.seek(metadata.getLinesIndexPosition());

		String tmp;

		tmp = filestream.readLine();
		tmp = tmp.trim();

		if (tmp.equals("indexlines")) {
			do {
				tmp = filestream.readLine();
				tmp = tmp.trim();
				if (tmp.contains(Commons.SEPARATOR + "")) {
					String[] tmp2 = tmp.split(Commons.SEPARATOR + "");
					if (tmp2.length >= 2) {
						long pos;
						int id;
						try {
							pos = Long.parseLong(tmp2[1]);
							id = Integer.parseInt(tmp2[0]);
							indexLineToPosition.put(id, pos);
						} catch (NumberFormatException ne) {
							// ignore
						}
					}
				}

			} while (!tmp.equals("end"));
		} else {
			throw new InvalidFileException("Expecting Lines Index at <"
					+ metadata.getLinesIndexPosition() + ">");
		}

	}

	/**
	 * Loads the (StopId -> StopPosition,SPTPosition) index
	 * 
	 * @throws IOException
	 */
	private void loadStopsIndex() throws IOException {
		if (metadata.getStopsIndexPosition() == -1) {
			throw new InvalidFileException("No stops index present");
		} else
			filestream.seek(metadata.getStopsIndexPosition());

		String tmp;

		tmp = filestream.readLine();
		tmp = tmp.trim();

		if (tmp.equals("indexstops")) {
			do {
				tmp = filestream.readLine();
				tmp = tmp.trim();
				if (tmp.contains(Commons.SEPARATOR + "")) {
					String[] tmp2 = tmp.split(Commons.SEPARATOR + "");
					if (tmp2.length >= 2) {
						int id;
						try {
							id = Integer.parseInt(tmp2[0]);
							String s = tmp2[1];
							s = s.trim();
							if (s.contains(",")) {
								String[] pos = s.split(",");
								if (pos.length >= 2) {
									indexStopToPosition.put(id, Long
											.parseLong(pos[0]));
									indexStopToTree.put(id, Long
											.parseLong(pos[1]));
								}
							}
						} catch (NumberFormatException ne) {
							// ignore
						}
					}
				}
			} while (!tmp.equals("end"));
			// vérifier que le nombre de stops est conforme
			if (metadata.getNbStops() != -1) {
				if (metadata.getNbStops() != indexStopToPosition.size()) {
					throw new InvalidFileException("Expected "
							+ metadata.getNbStops()
							+ " in the stops index. Found "
							+ indexStopToPosition.size());
				}
			}
		} else {
			throw new InvalidFileException("Expecting Stop Index at <"
					+ metadata.getStopsIndexPosition() + ">");
		}
	}

	/**
	 * Loads the (StopName -> stopId) index
	 * 
	 * @throws IOException
	 */
	private void loadStopsNamesIndex() throws IOException {
		if (metadata.getStopsNamesIndexPosition() == -1) {
			throw new InvalidFileException("No lines index present");
		} else
			filestream.seek(metadata.getStopsNamesIndexPosition());

		String tmp;

		tmp = filestream.readLine();
		tmp = tmp.trim();

		if (tmp.equals("indexstopsnames")) {
			do {
				tmp = filestream.readLineUTF();
				tmp = tmp.trim();
				if (tmp.contains(Commons.SEPARATOR + "")) {
					String[] tmp2 = tmp.split(Commons.SEPARATOR + "");
					if (tmp2.length >= 2) {
						String name;
						int id;
						try {
							name = tmp2[0];
							id = Integer.parseInt(tmp2[1]);
							indexNameToId.put(name, id);
						} catch (NumberFormatException ne) {
							// ignore
						}
					}
				}

			} while (!tmp.equals("end"));
			// vérifier que le nombre de stops est conforme
			if (metadata.getNbStops() != -1) {
				if (metadata.getNbStops() != indexNameToId.size()) {
					throw new InvalidFileException("Expected "
							+ metadata.getNbStops()
							+ " in the stops name index. Found "
							+ indexNameToId.size());
				}
			}
		} else {
			throw new InvalidFileException("Expecting Stops names Index at <"
					+ metadata.getStopsNamesIndexPosition() + ">");
		}
	}

	/**
	 * @return The list of all stops name in this map
	 */
	public ArrayList<String> getStopsList() {
		ArrayList<String> retVal = new ArrayList<String>();
		for (String s : indexNameToId.keySet()) {
			retVal.add(s);
		}
		return retVal;
	}

	/**
	 * @return the list of all lines in the map
	 */
	public Set<Integer> getLinesIdList() {
		return indexLineToPosition.keySet();
	}

	public Map<Integer, String> getLinesByStop(int stopId) throws IOException {
		Map<Integer, String> stopLines = new HashMap<Integer, String>();
		Set<String> stopLinesName = new HashSet<String>();
		for (int line : getLinesIdList()) {
			if (getLineFromId(line).getStopsList().contains(stopId)) {
				LineRecord myLine = getLineFromId(line);
				if (stopLinesName.add(myLine.getLineName())) {
					stopLines.put(line, myLine.toString());
				}
			}
		}
		return stopLines;
	}

	/**
	 * @param stopName
	 *            The name of the stop to look for
	 * @return The stop id of the stop named stopName or -1 if there's no such
	 *         stop
	 */
	public int getStopId(String stopName) {
		if (indexNameToId.containsKey(stopName)) {
			return indexNameToId.get(stopName);
		} else
			return -1;
	}

	/**
	 * Gets the Stop record from its ID
	 * 
	 * @param stopId
	 * @return
	 * @throws IOException
	 */
	public StopRecord getStopFromId(int stopId) throws IOException {
		long pos = getStopPosition(stopId);
		return loadStopRecord(pos);
	}

	/**
	 * Gets the Line record from its ID
	 * 
	 * @param lineId
	 * @return
	 * @throws IOException
	 */
	public LineRecord getLineFromId(int lineId) throws IOException {
		long pos = getLinePosition(lineId);
		return loadLineRecord(pos);
	}

	/**
	 * Load a line record at postion pos in the metroid file
	 * 
	 * @param pos
	 * @return
	 * @throws IOException
	 */
	public LineRecord loadLineRecord(long pos) throws IOException {
		LineRecord line = new LineRecord();
		// move to position
		filestream.seek(pos);

		String tmp = filestream.readLineUTF();
		tmp = tmp.trim();

		if (tmp.equals("line")) {
			// id
			tmp = filestream.readLineUTF().trim();
			line.setLineId(tmp);
			// name
			tmp = filestream.readLineUTF().trim();
			line.setLineName(tmp);
			// frequency
			tmp = filestream.readLineUTF().trim();
			line.setFrequency(tmp);
			// Terminus
			tmp = filestream.readLineUTF().trim();
			line.setTerminus(tmp);
			// stops list
			tmp = filestream.readLineUTF().trim();
			line.buildList(tmp);
			tmp = filestream.readLineUTF().trim();
			if (!tmp.equals("end")) {
				throw new InvalidFileException("Expected line record end at <"
						+ filestream.getFilePointer() + ">");
			}
		} else {
			throw new InvalidFileException("Expected line record at <" + pos
					+ ">");
		}

		return line;
	}

	public StopRecord loadStopRecord(long pos) throws IOException {
		StopRecord stop = new StopRecord();

		// move to position
		filestream.seek(pos);

		String tmp = filestream.readLineUTF();
		tmp = tmp.trim();

		if (tmp.equals("stop")) {
			// id
			tmp = filestream.readLineUTF().trim();
			stop.setStopId(tmp);
			// name
			tmp = filestream.readLineUTF().trim();
			stop.setStopName(tmp);
			// coordinates
			tmp = filestream.readLineUTF().trim();
			stop.setCoordinates(tmp);
			// lines ids
			tmp = filestream.readLineUTF().trim();
			stop.setLinesIds(tmp);
			// end
			tmp = filestream.readLineUTF().trim();
			if (!tmp.equals("end")) {
				throw new InvalidFileException("Expected stop record end at <"
						+ filestream.getFilePointer() + ">");
			}
		} else {
			throw new InvalidFileException("Expected stop record at <" + pos
					+ ">");
		}
		return stop;
	}

	/**
	 * 
	 * @param stopId
	 * @return The position of the stop record in the file or -1 if the id
	 *         doesn't exist
	 */
	public long getStopPosition(int stopId) {
		if (indexStopToPosition.containsKey(stopId)) {
			return indexStopToPosition.get(stopId);
		} else
			return -1;
	}

	/**
	 * @param lineId
	 * @return The position of the line record in the file or -1 if the id
	 *         doesn't exist
	 */
	public long getLinePosition(int lineId) {
		if (indexLineToPosition.containsKey(lineId)) {
			return indexLineToPosition.get(lineId);
		} else
			return -1;
	}

	/**
	 * Load the path between two stops identified by their ids. If there's no
	 * path an empty is returned. If the ids are wrong, a null value is returned
	 * 
	 * @param departureId
	 *            Departure Stop Id
	 * @param arrivalId
	 *            Arrival stop Id
	 * @return The list of line segments that constitute the path between the
	 *         two stops
	 * @throws IOException
	 *             If an error occurs during the retrieval
	 */
	public ArrayList<PathRecord> loadPath(int departureId, int arrivalId)
			throws IOException {
		ArrayList<PathRecord> retVal = new ArrayList<PathRecord>();
		List<PathRecord> tmpList = new ArrayList<PathRecord>(); // path are read
																// in reverse
																// order first

		if (isStreamClosed()) {
			openStream();
		}

		long treePos = -1;
		// retrieve tree location
		if (!indexStopToTree.containsKey(departureId))
			return null;
		else
			treePos = indexStopToTree.get(departureId);

		if (arrivalId >= getMetadata().getNbStops()) // invalid end stop
			return null;
		// ok. go to position
		filestream.seek(treePos);

		// read the "tree stopId" line
		String tmp = filestream.readLine();
		long treeStart = filestream.getFilePointer(); // start of first line
		tmp = tmp.trim();
		if (!tmp.startsWith("tree") || tmp.length() <= 4) {
			// error
			throw new InvalidFileException("Expected" + departureId
					+ " 'tree' at <" + treePos + ">.");
		}
		tmp = tmp.substring(4);
		tmp = tmp.trim();
		int id;
		try {
			id = Integer.parseInt(tmp);
			if (id != departureId) {
				throw new InvalidFileException("");
			}
		} catch (Exception ne) {
			// error
			throw new InvalidFileException("Expected" + departureId
					+ " 'tree' at <" + treePos + ">.");
		}

		// ok. good tree
		PathRecord rec = loadPathRecord(treeStart, arrivalId);
		while (rec.getPrevious() != null) {
			tmpList.add(rec);
			rec = loadPathRecord(treeStart, rec.getPrevious().getStopId());
		}
		if (rec.getPrevious() != null && rec.getPrevious().getStopId() == -1)
			// there's no path.return an empty list
			return retVal;

		// now reorder the records
		for (int i = tmpList.size() - 1; i >= 0; i--) {
			retVal.add(tmpList.get(i));
		}

		return retVal;
	}

	/**
	 * Load the path between two stops identified by their ids. If there's no
	 * path an empty is returned. If the names are wrong, a null value is
	 * returned
	 * 
	 * @param departure
	 *            Departure Stop Id
	 * @param arrival
	 *            Arrival stop Id
	 * @return The list of line segments that constitute the path between the
	 *         two stops
	 * @throws IOException
	 *             If an error occurs during the retrieval
	 */
	public ArrayList<PathRecord> loadPath(String departure, String arrival)
			throws IOException {
		int departureId = getStopId(departure);
		int arrivalId = getStopId(arrival);

		if (departureId == -1 || arrivalId == -1) {
			return null;
		} else
			return loadPath(departureId, arrivalId);
	}

	/**
	 * Loads a path record in a shortest path tree
	 * 
	 * @param startPos
	 * @param stopId
	 * @return
	 * @throws IOException
	 */
	private PathRecord loadPathRecord(long startPos, int stopId)
			throws IOException {
		int LINE = 6; // taille d'une ligne en octets
		PathRecord rec = new PathRecord();

		// first line : stopId 1
		// second line : stopId 2
		// /...

		// go to the corresponding line
		filestream.seek(startPos + LINE * (stopId - 1));

		int packedInt = filestream.readInt();
		short length = filestream.readShort();

		int lineId = packedInt & Commons.LINE_MASK;
		if (lineId == Commons.LINE_MASK)
			lineId = -1;

		int prevId = packedInt & Commons.PREV_MASK;
		if (prevId == Commons.PREV_MASK)
			prevId = -1;
		else
			prevId >>= 11;

		int id = packedInt & Commons.STOP_MASK;
		if (id == Commons.STOP_MASK)
			id = -1;
		else
			id >>= 21;

		if (id != stopId) {
			throw new InvalidFileException("");
		}

		if (lineId == -1)
			rec.setAfoot(true);
		else
			rec.setAfoot(false);
		if (id != -1)
			rec.setArrival(getStopFromId(id));
		else
			rec.setArrival(null);
		if (prevId != -1)
			rec.setPrevious(getStopFromId(prevId));
		else
			rec.setPrevious(null);
		rec.setLength(length);
		if (lineId != -1)
			rec.setLine(getLineFromId(lineId));
		else
			rec.setLine(null);

		return rec;

		/*
		 * String line = filestream.readLine(); line = line.trim();
		 * if(line.contains(Commons.SEPARATOR+"")){ String[] parts =
		 * line.split(Commons.SEPARATOR+""); int id; try{ id =
		 * Integer.parseInt(parts[0].trim()); if(id != stopId){ throw new
		 * InvalidFileException(""); } String[] tmp =
		 * parts[1].trim().split(","); if(tmp.length == 3) { int prevId =
		 * Integer.parseInt(tmp[0]); int length = Integer.parseInt(tmp[1]); int
		 * lineId = Integer.parseInt(tmp[2]);
		 * 
		 * if(lineId == -1) rec.setAfoot(true); else rec.setAfoot(false); if(id !=
		 * -1) rec.setArrival(getStopFromId(id)); else rec.setArrival(null);
		 * if(prevId != -1) rec.setPrevious(getStopFromId(prevId)); else
		 * rec.setPrevious(null); rec.setLength(length); if(lineId != -1)
		 * rec.setLine(getLineFromId(lineId)); else rec.setLine(null);
		 * 
		 * return rec;
		 *  } else{ throw new InvalidFileException(""); } } catch(Exception e){
		 * throw new InvalidFileException("At <"+startPos+LINE*(stopId-1)+"> :
		 * invalid tree line "); } } else throw new InvalidFileException("At
		 * <"+startPos+LINE*(stopId-1)+"> : invalid tree line ");
		 */
	}

}
